fix(cuda.bindings): Make param_packer.feed() free-threading-safe via import-time init - #2417
fix(cuda.bindings): Make param_packer.feed() free-threading-safe via import-time init#2417clin1234 wants to merge 18 commits into
Conversation
Restructure param_packer.h so the ctypes type pointers and the feeder table are built once in a new init_param_packer(), called at module import (from utils.pxi) while single-threaded. feed() becomes a read-only lookup on a never-mutated std::map, so concurrent kernel launches from multiple host threads no longer race the map or the ctypes lazy-init -- races that were live in the cp315t (Py_MOD_GIL_NOT_USED) wheels. init_param_packer is declared 'except +' so a failed 'import ctypes' surfaces as a Python exception at import instead of std::terminate across the Cython boundary, and feed() is now non-throwing. The ctypes module strong ref is kept deliberately (it keeps the borrowed type pointers valid). Harden resource_handles.cpp: make mr_dealloc_cb std::atomic with acquire/release ordering, and document the verified single-init of initialize_deferred_cleanup() and the mutation-only lazy path for GraphBox::slot_table (both already safe; no behavior change).
seberg
left a comment
There was a problem hiding this comment.
Thanks, for finding/fixing these issues. Moving the ctypes import/populating to import time seems OK to me (at worst 5% seems like additional import time).
I hate to ask it, but touching this, it would be nice to clean up the param packer initialization a bit more.
That said, the other two changes seem a bit strange to me and maybe because of merge conflicts? (I.e. the graphbox clearly won't compile)
| if (mr_dealloc_cb) { | ||
| mr_dealloc_cb(mr, b->resource, size, b->h_stream); | ||
| if (MRDeallocCallback cb = | ||
| mr_dealloc_cb.load(std::memory_order_acquire)) { |
There was a problem hiding this comment.
I am not sure I am following, this seems like unnecessary hardening?
If this was a general pattern, I might not mind, but I think we have many of these globals that we assume are initialized once at module init time and if that is the only time it is set, it is fine?
| void init_param_packer() except + | ||
| # Hot path: read-only lookup, does not throw -> intentionally left as an | ||
| # implicit-noexcept extern (no `except +` needed). | ||
| int feed(void* ptr, object o, object ct) |
There was a problem hiding this comment.
This looks like a regression?
You are right, this function can get away without the except + (if we can, I wouldn't mind encoding that on the C++ side for clarity, though).
But, it clearly needs to have the except -1! (you can drop the ? not that it matters)
I don't know if your analysis was based on some old branch where Python errors were not anticipated?!
| # Populates ctypes pointers + the feeder table once, at import. `except +` | ||
| # translates a C++ throw (e.g. failed `import ctypes`) into a Python | ||
| # exception during module import, preserving any already-set Python error. | ||
| void init_param_packer() except + |
There was a problem hiding this comment.
| void init_param_packer() except + | |
| void init_param_packer() except +* |
(For me, I would personally like if it actually propagated python errors right away.)
| { | ||
| m_feeders[{target_t,source_t}] = [](void* ptr, PyObject* value) -> int | ||
| { | ||
| *((long long*)ptr) = (long long)PyLong_AsLongLong(value); |
There was a problem hiding this comment.
This is missing the error check :/!
| // load-bearing: the ctypes_c_* pointers below are *borrowed* references into | ||
| // the ctypes module dict, and stay valid only while this strong ref keeps the | ||
| // module (and therefore its type objects) alive. Do not Py_DECREF it. | ||
| static PyObject* ctypes_module = nullptr; |
There was a problem hiding this comment.
If we re-wire everything here, can we just remove this global and make all type objects strong references instead? That seems much clearer.
(Unfortunately, I guess we are not vendoring pythoncapi-compat, so we don't have PyDict_GetItemStringRef, here it's fine, so let's not worry, although I could see that we'll just need that in the future...)
…NVIDIA#2238) Writing into a host-accessible array obtained via np.from_dlpack requires NumPy 2.2.5+, because earlier versions return a read-only array (numpy GH #28632). Several tests and examples guarded these writes at NumPy 2.1.0, so on NumPy 2.1.0-2.2.4 they would error instead of skip. Audit all NumPy version guards in cuda_core and bump the write-into-DLPack sites to 2.2.5 with a consistent reason matching the existing correct guard in test_launcher.py. Guards that only read DLPack arrays (e.g. strided_memory_view_constructors.py) or skip for an unrelated NumPy fix (test_utils.py, numpy#26501) are left at 2.1. Also fix examples/memory_ops.py, which used a plain string comparison (np.__version__ < "2.1.0") that is unreliable for multi-digit versions; replace it with np.lib.NumpyVersion to match the other examples, and standardize test_graph_builder.py off a hand-rolled tuple skipif onto requires_module. Update affected PEP 723 dependency pins to numpy>=2.2.5. Signed-off-by: Arsalan Shakil <shakil.arsalan@yahoo.com>
…iptors (NVIDIA#2376) * Pathfinder: add support on cutensornet for library loading and header descriptors * Pathfinder: add support on custatevec for library loading and header descriptors * Pathfinder: add support on cudensitymat for library loading and header descriptors * Pathfinder: add support on custabilizer for library loading and header descriptors * Pathfinder: add support on cupauliprop for library loading and header descriptors * switch to cuquantum metapackage to simplify dependency listing --------- Co-authored-by: Ralf W. Grosse-Kunstleve <rwgkio@gmail.com>
…aders (NVIDIA#2239) * fix(pathfinder): remove dead/misleading error handling in platform loaders Two dead error-handling paths in the dynamic-lib platform loaders: - load_dl_linux.load_with_system_search guarded abs_path with 'if abs_path is None: raise RuntimeError("No expected symbol ...")'. abs_path_for_dynamic_library never returns None (it returns a resolved path or raises OSError), so the branch is unreachable and its message is factually wrong (it concerns dlinfo path resolution, not symbols). Drop the dead branch and let the descriptive OSError surface, matching the deterministic-loader policy of not masking discovery/load failures. - load_dl_windows.add_dll_directory had 'if not result: pass', a no-op; the PATH update below already runs unconditionally. Remove the dead branch and clarify the comment on why PATH is updated regardless. No behavior change: both removed branches were unreachable or no-ops. Signed-off-by: Aryan <aryansputta@gmail.com> * fix(pathfinder): simplify loader review comments --------- Signed-off-by: Aryan <aryansputta@gmail.com> Co-authored-by: Ralf W. Grosse-Kunstleve <rwgkio@gmail.com>
…VIDIA#2196) * fix(pathfinder): make find_nvidia_binary_utility deterministic, never search CWD find_nvidia_binary_utility assembled a bounded list of trusted directories (NVIDIA wheel bin/, CONDA_PREFIX, CUDA_HOME/CUDA_PATH) and then delegated to shutil.which(name, path=trusted_dirs). On Windows shutil.which prepends the process current working directory to the search even when an explicit path= is supplied, so a binary located in an arbitrary (possibly attacker-writable) CWD could be returned in preference to the trusted CUDA / Conda / wheel binary. That violates the pathfinder contract of a deterministic lookup over a documented, bounded set of trusted roots. Replace the shutil.which delegation with an explicit resolver that searches only the trusted directories, in order, returning the first executable match. The current working directory and ambient PATH are never consulted. POSIX execute-bit (X_OK) and Windows extension semantics are preserved, so behavior is unchanged except for removing the CWD/PATH leakage. Names resolved in the existing trusted dirs return exactly as before. Rewrites the search-path tests to assert the deterministic probe order and adds TestResolveInTrustedDirs covering CWD isolation, first-match-wins, empty/duplicate dir skipping, and POSIX non-executable rejection. Fixes NVIDIA#2119 * feat(pathfinder): add CTK-root canary fallback to find_nvidia_binary_utility After the deterministic search over the explicit trusted directories (NVIDIA wheel bin/, CONDA_PREFIX, CUDA_HOME/CUDA_PATH) misses, fall back to a CTK-root canary probe: resolve cudart through the OS dynamic loader, which honors LD_LIBRARY_PATH on Linux and the native DLL search on Windows, derive the CUDA Toolkit root from its absolute path, and search that root's bin layout. This addresses the concern raised on NVIDIA#2196: users who follow the CUDA Linux installation guide set LD_LIBRARY_PATH for libraries and PATH for executables. The bounded finder alone would stop finding the utility for them because PATH is intentionally never consulted. The canary fallback recovers that case through LD_LIBRARY_PATH instead of PATH. LD_LIBRARY_PATH is still an attack vector, but a significantly harder one to exploit than PATH, and the ambient PATH and process CWD remain unused. The canary runs only after the explicit trusted dirs miss, so the common wheel/conda/CUDA_HOME cases never spawn the resolver subprocess. The canary -> CTK-root resolution is factored into a shared resolve_ctk_root_via_canary helper reused by the dynamic-library CTK-root canary flow. Adds tests for the fallback (found, ordering, Windows bin layout, not consulted when found earlier, cached) and for resolve_ctk_root_via_canary. Adds 1.6.0 release notes for the minor version bump. * fix(pathfinder): satisfy mypy no-any-return in canary helpers pre-commit.ci mypy flagged returning Any from resolve_ctk_root_via_canary and _resolve_ctk_root_via_canary (both declared -> str | None), because derive_ctk_root resolves to Any under the pathfinder mypy config. Bind the result to an annotated local before returning, matching the pattern used elsewhere in the package. * fix(pathfinder): address binary finder review * docs(pathfinder): prepare 1.6.0 release notes * refactor(pathfinder): apply review feedback on binary finder - Rename _is_executable_file to _is_executable_candidate; the helper marks a return candidate rather than proving OS executability. Drop its docstring now that the name is self-explanatory, and update the test patch target. - Drop the 'ambient PATH is never consulted' sentence from resolve_ctk_root_via_canary; it is not universally true. Signed-off-by: Aryan <aryansputta@gmail.com> --------- Signed-off-by: Aryan <aryansputta@gmail.com> Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
…VIDIA#2396) Pixi <0.71.0 re-ran the editable source build on every `pixi run`, recompiling all ~29 Cython extensions in cuda_bindings (~27s) even when nothing changed, because setuptools' editable_wheel uses fresh temp build dirs so build_ext always sees the .so as older than its source. Pixi 0.71.0 fixed this upstream with a content-addressed source-build cache (prefix-dev/pixi#6285) plus separate Cython input tracking (prefix-dev/pixi#6123). Locally verified: under 0.66 the 2nd no-op run rebuilds 29 extensions with a fresh /tmp/*.build-temp; under 0.73 it rebuilds nothing and returns instantly. The fix also bumps the pixi.lock format from v6 to v7 (unreadable by pixi 0.66), so this requires all contributors to move to pixi >=0.71. Regenerated all six committed lock files to v7 accordingly. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…A#2357) * test(cuda.core): define graph attachment ownership requirements Preserve the validated lifetime and failure scenarios as executable specifications for the metadata redesign, alongside independent cleanup clarifications. * refactor(cuda.core): define graph hierarchy storage Establish canonical per-graph boxes, stable hierarchy ownership, and Level 1 graph identity before rebuilding attachment operations. * refactor(cuda.core): canonicalize graph hierarchy handles Give root and child graph handles stable per-graph boxes while sharing one hierarchy control block and registry identity. * refactor(cuda.core): retain immutable node attachments Replace the mutable graph-wide slot table with complete per-node user objects and intrusive deferred cleanup. * refactor(cuda.core): import cloned graph attachments Copy and rekey source attachment metadata into embedded graph hierarchies before publishing their canonical boxes. * refactor(cuda.core): invalidate destroyed child graph state Unregister and tombstone child graph boxes after CUDA destroys their owner while preserving stable storage for existing handles. * fix(cuda.core): retain unidentified captured callbacks Allow graph-level anonymous attachments so a captured host callback stays safe when CUDA commits it but its node cannot be recovered. * test(cuda.core): cover graph hierarchy invalidation Verify anonymous capture retention and proactive invalidation of child and conditional graph views. * docs(cuda.core): explain graph attachment ownership Orient contributors to versioned node ownership, graph hierarchy metadata, clone propagation, invalidation, and deferred cleanup. * fix(cuda.core): harden graph attachment lifecycle Make node identity atomic, preserve tombstoned graph identity, retry deferred cleanup safely, and cover final-reference launch ownership. * docs(cuda.core): add 1.2.0 graph lifetime note Document corrected graph attachment lifetime and callback-safe resource release. * refactor(cuda.core): prepare attachments before graph mutation Retain attachment owners and preallocate metadata before CUDA graph mutations so failures cannot leave nodes with unretained resources. * fix(cuda.core): make attachment rollback portable Carry rollback through the prepared attachment deleter so consumer extension modules do not depend on an out-of-line C++ symbol. * docs(cuda.core): clarify graph handle semantics Preserve the general no-invalidation guarantee while documenting borrowed child graphs as a narrow driver-owned exception, and tighten implementation comments for reviewers. * docs(cuda.core): clarify graph attachment ownership Reframe the design around parameter versions and driver-managed lifetimes, and keep mutation ordering guidance beside the implementation. * docs(cuda.core): add graph ownership diagrams Replace the ASCII sketch with Mermaid and illustrate the GraphHandle alias and control-block relationships for reviewers. * docs(cuda.core): warn about graph attachment cycles Document that driver-held Python references are opaque to cyclic GC and identify affected graph APIs.
* New API for nvrtc * Apply suggestion from @mdboom
…p helper lifetime (NVIDIA#2399) * Harden program-cache perms, binary-path resolution, and coredump helper lifetime - cuda.core: create the on-disk program cache tree owner-only (0o700) and re-assert restrictive perms on POSIX, so cached device code cannot be read or planted by other local users regardless of the inherited umask. - cuda.pathfinder: absolutize the resolved binary-utility path to honor the documented absolute-path contract; surface AddDllDirectory failures on Windows with GetLastError instead of silently swallowing them. - cuda.bindings: retain the caller's bytes in _HelperCUcoredumpSettings so the borrowed pointer cannot outlive its backing buffer, and free the getter's 1 KiB buffer in __dealloc__; clarify get_buffer_pointer's lifetime contract. Adds regression tests for the cache permissions, path absolutization, and coredump helper lifetime. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Simplify code comments on the hardening fixes Shorten the explanatory comments to a line or two each and drop the internal issue-number references (which don't resolve in this repo). No code changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Remove agent_authored markers from the new tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Remove weak coredump buffer-lifetime test It didn't actually guard the two lifetime fixes: the driver copies the path during the set call (so the NVIDIA#379 latent UAF can't trigger), and a missing free leaks silently (so NVIDIA#381 wouldn't fail the test either). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Silence bandit S103 on the intentionally world-writable test dir The test pre-creates a 0o777 cache dir to verify the loader tightens it to 0o700; the permissive mask is the point of the test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: expect absolutized binary path on Windows The abspath change makes find_nvidia_binary_utility return a drive-qualified path on Windows (C:\... ), so the three search-order tests must compare against os.path.abspath(expected). No-op on Linux; fixes the Windows CI failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Raise on out-of-range c_int/c_byte kernel arguments instead of truncating The c_int/c_byte param feeders silently narrowed an out-of-range Python int (e.g. 2**32+5 -> 5). Range-check in the feeder and raise OverflowError; declare feed() as 'except? -1' so Cython propagates the exception instead of swallowing it. Addresses Glasswing V9.1 (leofang chose the raise option over matching ctypes' silent wrap). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * param_packer: use PyLong_AsInt for c_int range check on Python 3.13+ Per Leo's review: on 3.13+ PyLong_AsInt converts and range-checks in one call (raising OverflowError itself), so gate the manual INT_MIN/INT_MAX check to pre-3.13 only. c_byte keeps the manual check (no 8-bit CPython equivalent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * param_packer: unify c_int/c_byte range check via PyLong_AsLongAndOverflow Per Leo's follow-up: use a single code path on all supported Pythons instead of version-gating PyLong_AsInt. PyLong_AsLongAndOverflow flags out-of-long values via its overflow out-param (no exception set), then we bounds-check the 32-bit (c_int) / 8-bit (c_byte) target range and raise OverflowError. Drops the PY_VERSION_HEX gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Split kernel-arg overflow change (NVIDIA#363) into its own PR (NVIDIA#2402) The out-of-range c_int/c_byte -> OverflowError change is a distinct behavior change; moved to a standalone PR so it can be reviewed separately from this hardening batch. No functional change to the remaining hardening work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address review: scope cache perms to tmp, revert cybind docstring Per @leofang's review on PR NVIDIA#2399: - _file_stream.py: only the tmp/ staging dir is created owner-only (0o700). root/ and entries/ now inherit the umask / any pre-existing permissions so a deliberately shared kernel cache (e.g. group-writable on a compute cluster) keeps working. Dropped the post-mkdir chmod that would have re-tightened an existing shared directory. 0o700 in mkdir mode needs no chmod: umask only clears bits. - _internal/utils.pyx: revert the get_buffer_pointer docstring change; that is cybind code and is out of scope for this PR. - Tests updated to match: assert only tmp is 0o700, and that a pre-existing 0o777 shared root is used as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Document accepted security trade-off for shared program caches Record the residual risk from narrowing the cache-permission hardening (glasswing NVIDIA#359/NVIDIA#375) per PR NVIDIA#2399 review: committed entry files stay 0o600 (contents owner-only via mkstemp+os.replace), tmp/ is owner-only to protect in-flight writes, but root/entries inherit the umask so shared caches keep working. In a group-writable shared cache a group member could replace a cached kernel; tamper-proofing that path needs load-time integrity checks and is out of scope here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Simplify cache permission comment; drop internal tool name Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Strip internal issue numbers from cache permission test docstring Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * pathfinder: use os.add_dll_directory() instead of raw kernel32 call Per @leofang's review question: replace the direct ctypes kernel32.AddDllDirectory() call with the stdlib os.add_dll_directory() wrapper. Both call the same Win32 API, but the stdlib version drops the hand-maintained argtypes/restype binding and reports failure as OSError instead of a NULL cookie + GetLastError() check. Behavior is unchanged: the returned handle is intentionally discarded (it has no finalizer, so the directory stays on the search path for the process lifetime), and the PATH mutation + failure warning are preserved. Also strip an internal issue number from a test docstring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…VIDIA#2242) * toolshed: handle anonymous struct renames in check_cython_abi.py The name of an anonymous struct is an implementation detail, so it is acceptable for the name to change between builds (e.g. from the old anon_structXX / anon_unionXX scheme to the newer MODULE__anon_podXX scheme that Cython now emits). This change makes the checker verify that anonymous structs have the same *structure*, while tolerating name changes, so spurious "Missing" / "Added" errors are no longer reported for pure renames. Three improvements: 1. `_format_base_type_name`: unwrap `CConstOrVolatileTypeNode` instead of falling through to its class name. Const/volatile qualifiers do not affect ABI layout, and the old behaviour stored the Python class name literally ("CConstOrVolatileTypeNode*") in generated .abi.json files, which caused field-type mismatches when comparing builds where the qualifier was expressed differently. 2. `_build_anon_rename_map`: new iterative bottom-up pass that builds a mapping from old-style anon names (expected) to new-style anon_pod names (found) by matching field content. Leaf structs (no anon-type fields) are matched first; each round normalises the remaining candidates using already-known renames, allowing parent structs that embed renamed children to match in subsequent rounds. 3. `_normalize_type`: uses `re.sub` with `\b` word-boundary anchors instead of plain `str.replace`, so a shorter name like "anon_struct1" cannot corrupt a longer one like "anon_struct12" if iteration order happens to process the shorter key first. `check_structs` is updated to look up each expected struct through the rename map, normalise expected field types before comparing, and skip the "Added" report for new-style names that are confirmed renames. Note: baselines generated with the old tool (containing "CConstOrVolatileTypeNode*" type strings) must be regenerated with the fixed tool before the rename-matching logic can work correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Ignore private parts of the ABI --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(cuda.core): isolate pending-call saturation cleanup Run the process-global queue probe in a subprocess and explicitly drain its synthetic callbacks so parallel free-threaded tests cannot contaminate one another. * test(cuda.core): keep subprocess probe self-contained Pass the finalization timeout into the child so process isolation does not depend on importing test helpers from the temporary working directory.
…#2339) * add an api to return module in ObjectCode to support calling legacy driver APIs * cuda.core: clarify ObjectCode.handle vs get_module() docstrings Document that .handle returns the native context-independent CUlibrary for cuda.core/newer APIs, while get_module() is a legacy-interop bridge returning a context-dependent CUmodule via cuLibraryGetModule.
…IDIA#2402) * Raise OverflowError on out-of-range c_int/c_byte kernel arguments An out-of-range Python int passed as a c_int/c_byte kernel argument was silently narrowed (e.g. 2**32+5 -> 5). The feeders now range-check via PyLong_AsLongAndOverflow + an explicit 32-bit/8-bit bounds check and raise OverflowError; feed() is declared 'except? -1' so Cython propagates the exception instead of swallowing it. Adds a GPU regression test. Addresses Glasswing finding V9.1; behavior reviewed and shaped by @leofang (single code path via PyLong_AsLongAndOverflow). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * param_packer: clarify why c_int/c_byte bound to int range, not long A reviewer asked why the manual check uses INT_MIN/INT_MAX rather than the function's long-based overflow flag. Add a comment: the target slot is 32-bit (8-bit for c_byte), and long is 64-bit on LP64, so overflow alone misses 2**31..2**63 -- the explicit int-range check catches those. Comment-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * param_packer: use PyLong_AsInt for c_int range check (backport for <3.13) Address review feedback on NVIDIA#2402: replace the hand-rolled c_int bounds check with CPython's PyLong_AsInt, which does exactly the INT_MIN/INT_MAX range check we want and raises OverflowError itself. PyLong_AsInt only became public in Python 3.13, so add a file-local, version-gated backport (a copy of the CPython implementation, made `static` because this header is compiled into every extension module that includes it). The c_byte feeder has no CPython equivalent, so it keeps the explicit PyLong_AsLongAndOverflow bounds check; its comment is now self-contained rather than referencing the (now-changed) c_int feeder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
81794c3 to
cb62ef7
Compare
|
pre-commit.ci autofix |
Description
Restructure param_packer.h so the ctypes type pointers and the feeder table are built once in a new init_param_packer(), called at module import (from utils.pxi) while single-threaded. feed() becomes a read-only lookup on a never-mutated std::map, so concurrent kernel launches from multiple host threads no longer race the map or the ctypes lazy-init -- races that were live in the cp315t (Py_MOD_GIL_NOT_USED) wheels. init_param_packer is declared 'except +' so a failed 'import ctypes' surfaces as a Python exception at import instead of std::terminate across the Cython boundary, and feed() is now non-throwing. The ctypes module strong ref is kept deliberately (it keeps the borrowed type pointers valid).
Harden resource_handles.cpp: make mr_dealloc_cb std::atomic with acquire/release ordering, and document the verified single-init of initialize_deferred_cleanup() and the mutation-only lazy path for GraphBox::slot_table (both already safe; no behavior change).
closes #2416